1use crate::copp::copp3::Topp3Profile;
28use crate::copp::copp3::formulation::{Topp3Problem, get_weight_a_topp3};
29use crate::copp::copp3::opt3::ClarabelExpertInfor3rd;
30use crate::copp::copp3::opt3::clarabel_constraints::{
31 clarabel_standard_capacity_topp3, clarabel_standard_constraint_topp3,
32};
33use crate::copp::{ClarabelOptions, clarabel_to_copp3_solution};
34use crate::diag::{
35 CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
36 check_boundary_state_copp3_valid, check_s_interval_valid, format_duration_human,
37};
38use clarabel::algebra::CscMatrix;
39use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
40use core::f64;
41
42pub fn topp3_lp(
61 problem: &Topp3Problem,
62 options: &ClarabelOptions,
63) -> Result<Topp3Profile, CoppError> {
64 let (result, solution) = topp3_lp_expert(problem, options)?;
65 result.ok_or_else(|| CoppError::ClarabelSolverStatus("topp3_lp".into(), solution.status))
66}
67
68pub fn topp3_lp_expert(
92 problem: &Topp3Problem,
93 options: &ClarabelOptions,
94) -> Result<(Option<Topp3Profile>, DefaultSolution<f64>), CoppError> {
95 let info = topp3_lp_expert_with_info(problem, options)?;
96 let _ = &info.linsolver;
97 Ok((info.result, info.solution))
98}
99
100pub fn topp3_lp_expert_with_info(
106 problem: &Topp3Problem,
107 options: &ClarabelOptions,
108) -> Result<ClarabelExpertInfor3rd, CoppError> {
109 match options.verbosity() {
110 Verbosity::Silent => topp3_lp_core(problem, (options, SilentVerboser)),
111 Verbosity::Summary => topp3_lp_core(problem, (options, SummaryVerboser::new())),
112 Verbosity::Debug => topp3_lp_core(problem, (options, DebugVerboser::new())),
113 Verbosity::Trace => topp3_lp_core(problem, (options, TraceVerboser::new())),
114 }
115}
116
117fn topp3_lp_core(
128 problem: &Topp3Problem,
129 options_verboser: (&ClarabelOptions, impl Verboser),
130) -> Result<ClarabelExpertInfor3rd, CoppError> {
131 let (options, mut verboser) = options_verboser;
132 let idx_s_start = problem.idx_s_start;
133 let a_boundary = problem.a_boundary;
134 let b_boundary = problem.b_boundary;
135 let num_stationary = problem.num_stationary;
136 if verboser.is_enabled(Verbosity::Summary) {
137 verboser.record_start_time();
138 }
139 if verboser.is_enabled(Verbosity::Trace) {
140 let settings = options.clarabel_settings();
141 crate::verbosity_log!(
142 crate::diag::Verbosity::Summary,
143 "topp3_lp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
144 options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
145 options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
146 options.is_allow(clarabel::solver::SolverStatus::MaxTime),
147 options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
148 options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
149 settings.tol_gap_rel,
150 settings.tol_feas,
151 settings.max_iter,
152 settings.verbose
153 );
154 }
155
156 check_boundary_state_copp3_valid(a_boundary, b_boundary)?;
158 let n = problem.a_linearization.len() - 1;
159 let idx_s_final = idx_s_start + n;
160 if verboser.is_enabled(Verbosity::Summary) {
161 crate::verbosity_log!(
162 crate::diag::Verbosity::Summary,
163 "\ntopp3_lp started: {} <= idx_s <= {}, s_len = {}, num_stationary={:?}.",
164 idx_s_start,
165 idx_s_final,
166 problem.a_linearization.len(),
167 num_stationary
168 );
169 }
170 check_s_interval_valid("topp3_lp", idx_s_start, idx_s_final)?;
171 let (capacity_val, capacity_b, capacity_cones) =
177 clarabel_standard_capacity_topp3(problem.constraints, (idx_s_start, idx_s_final));
178 if verboser.is_enabled(Verbosity::Debug) {
179 crate::verbosity_log!(
180 crate::diag::Verbosity::Summary,
181 "topp3_lp: capacity estimate standard(val={capacity_val}, b={capacity_b}, cone={capacity_cones}), n_var={}",
182 2 * (n + 1)
183 );
184 }
185 let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(capacity_cones);
186 let mut row = Vec::<usize>::with_capacity(capacity_val);
187 let mut col = Vec::<usize>::with_capacity(capacity_val);
188 let mut val = Vec::<f64>::with_capacity(capacity_val);
189 let mut b = Vec::<f64>::with_capacity(capacity_b);
190 if verboser.is_enabled(Verbosity::Trace) {
191 crate::verbosity_log!(
192 crate::diag::Verbosity::Summary,
193 "topp3_lp: allocated capacities row/col/val/b/cones <= {capacity_val}/{capacity_val}/{capacity_val}/{capacity_b}/{capacity_cones}",
194 );
195 }
196
197 let s = problem.constraints.s_vec(idx_s_start, idx_s_final + 1)?;
199 let row_before_std = row.len();
200 let col_before_std = col.len();
201 let val_before_std = val.len();
202 let b_before_std = b.len();
203 let cones_before_std = cones.len();
204 clarabel_standard_constraint_topp3(
205 problem,
206 &s,
207 (&mut row, &mut col, &mut val, &mut b, &mut cones),
208 num_stationary,
209 &verboser,
210 )?;
211 if verboser.is_enabled(Verbosity::Trace) {
212 crate::verbosity_log!(
213 crate::diag::Verbosity::Summary,
214 "topp3_lp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
215 row.len() - row_before_std,
216 col.len() - col_before_std,
217 val.len() - val_before_std,
218 b.len() - b_before_std,
219 cones.len() - cones_before_std
220 );
221 }
222
223 let n_var = 2 * (n + 1);
225 let row_len = row.len();
226 let col_len = col.len();
227 let val_len = val.len();
228 let b_len = b.len();
229 let cones_len = cones.len();
230 let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
231 let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
233 let q_object = clarabel_q_object_topp3_lp(&s, num_stationary, n_var);
234 if verboser.is_enabled(Verbosity::Trace) {
235 let (q_min, q_max) = q_object
236 .iter()
237 .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
238 (mn.min(v), mx.max(v))
239 });
240 crate::verbosity_log!(
241 crate::diag::Verbosity::Summary,
242 "topp3_lp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}, q_range=[{}, {}]",
243 b_len,
244 n_var,
245 a_csc.nnz(),
246 p_object.nnz(),
247 q_min,
248 q_max
249 );
250 }
251 if verboser.is_enabled(Verbosity::Summary) {
252 crate::verbosity_log!(
253 crate::diag::Verbosity::Summary,
254 "topp3_lp: ready to solve with row/col/val/b/cones = {row_len}/{col_len}/{val_len}/{b_len}/{cones_len} and n_var = {n_var}.",
255 );
256 }
257 let settings = options.clarabel_settings().clone();
259 let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
260 .map_err(|e| CoppError::ClarabelSolverError("topp3_lp".into(), e))?;
261 solver.solve();
262 let linsolver = solver.info.linsolver.clone();
263 let solution = solver.solution;
264 if verboser.is_enabled(Verbosity::Summary) {
265 crate::verbosity_log!(
266 crate::diag::Verbosity::Summary,
267 "topp3_lp: solve done, status = {:?}, elapsed = {}.",
268 solution.status,
269 format_duration_human(verboser.elapsed())
270 );
271 }
272 if verboser.is_enabled(Verbosity::Trace) {
273 let show = solution.x.len().min(3);
274 crate::verbosity_log!(
275 crate::diag::Verbosity::Summary,
276 "topp3_lp: solution x_len={}, head={:?}",
277 solution.x.len(),
278 &solution.x[0..show]
279 );
280 }
281 let result = if options.is_allow(solution.status) {
282 Some(clarabel_to_copp3_solution(
283 &solution.x.as_slice()[0..2 * (n + 1)],
284 &s,
285 num_stationary,
286 ))
287 } else {
288 None
289 };
290 if verboser.is_enabled(Verbosity::Trace) {
291 crate::verbosity_log!(
292 crate::diag::Verbosity::Summary,
293 "topp3_lp: allow(status)={}, extracted_profile={}",
294 options.is_allow(solution.status),
295 if result.is_some() {
296 "Some(Topp3Profile)"
297 } else {
298 "None"
299 }
300 );
301 }
302 Ok(ClarabelExpertInfor3rd {
303 result,
304 solution,
305 linsolver,
306 })
307}
308
309#[inline(always)]
319fn clarabel_q_object_topp3_lp(s: &[f64], num_stationary: (usize, usize), n_var: usize) -> Vec<f64> {
320 let mut q_object = get_weight_a_topp3(s, num_stationary);
321 q_object.iter_mut().for_each(|q_i| *q_i = -*q_i);
323 q_object.resize(n_var, 0.0);
324 q_object
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::copp::ClarabelOptionsBuilder;
331 use crate::copp::InterpolationMode;
332 use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
333 use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
334 use crate::copp::copp2::stable::topp2_ra::topp2_ra;
335 use crate::copp::copp3::stable::basic::{Topp3ProblemBuilder, s_to_t_topp3, t_to_s_topp3};
336 use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
337 use crate::robot::robot_core::Robot;
338 use std::time::Instant;
339
340 #[test]
341 fn test_topp3_lp() -> Result<(), CoppError> {
342 run_test_topp3_lp_repeated(1, false)
343 }
344
345 #[test]
348 #[ignore = "slow"]
349 fn test_topp3_lp_robust() -> Result<(), CoppError> {
350 run_test_topp3_lp_repeated(100, true)
351 }
352
353 fn run_one_topp3_lp_case(
354 options_lp: &ClarabelOptions,
355 ) -> Result<(f64, f64, f64, f64, f64, usize), CoppError> {
356 let n: usize = 1000;
357 let dim = 7;
358 let mut rng = rand::rng();
359 let (s, path, _, _) =
360 lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
361
362 let mut robot = Robot::with_capacity(dim, n);
363 robot
364 .with_s(&s.as_view())?
365 .with_q_from_path_3rd(&path, 0, n)?;
366 add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0))?;
367
368 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
369 let start = Instant::now();
370 let options_ra = ReachSet2OptionsBuilder::new()
371 .lp_feas_tol(1E-9)
372 .a_cmp_abs_tol(1E-9)
373 .a_cmp_rel_tol(1E-9)
374 .build()?;
375 let a_profile_ra = topp2_ra(&topp2_problem, &options_ra)?;
376 let time_topp_ra = start.elapsed().as_secs_f64() * 1E3;
377 let (t_motion_ra, _) = s_to_t_topp2(s.as_slice(), &a_profile_ra, 0.0)?;
378
379 let start = Instant::now();
380 robot.constraints.amax_substitute(&a_profile_ra, 0)?;
381 let topp3_problem =
382 Topp3ProblemBuilder::new(&mut robot, 0, &a_profile_ra, (0.0, 0.0), (0.0, 0.0))
383 .with_num_stationary_max(2)
384 .build_with_linearization()?;
385 let profile = topp3_lp(&topp3_problem, options_lp)?;
386 let time_topp3_lp = start.elapsed().as_secs_f64() * 1E3;
387 let start = Instant::now();
388 let (t_motion_lp, t_s) = s_to_t_topp3(s.as_slice(), profile.as_parts(), 0.0)?;
389 let s_t = t_to_s_topp3(
390 s.as_slice(),
391 profile.as_parts(),
392 &t_s,
393 InterpolationMode::UniformTimeGrid(0.0, 1E-3, true),
394 )?;
395 let time_interpolation = start.elapsed().as_secs_f64() * 1E3;
396 Ok((
397 time_topp_ra,
398 time_topp3_lp,
399 time_interpolation,
400 t_motion_ra,
401 t_motion_lp,
402 s_t.len(),
403 ))
404 }
405
406 fn run_test_topp3_lp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
407 let options_lp = ClarabelOptionsBuilder::new()
408 .allow_almost_solved(true)
409 .build()?;
410
411 let mut tc_sum_ra = 0.0;
412 let mut tc_sum_lp = 0.0;
413 let mut tf_sum_ra = 0.0;
414 let mut tf_sum_lp = 0.0;
415 for i_exp in 0..n_exp {
416 let (
417 time_topp_ra,
418 time_topp3_lp,
419 time_interpolation,
420 t_motion_ra,
421 t_motion_lp,
422 s_t_len,
423 ) = run_one_topp3_lp_case(&options_lp)?;
424
425 if flag_print_step {
426 crate::verbosity_log!(
427 crate::diag::Verbosity::Summary,
428 "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_interpolation = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, s_t.len() = {}",
429 i_exp + 1,
430 time_topp_ra,
431 time_topp3_lp,
432 time_interpolation,
433 t_motion_ra,
434 t_motion_lp,
435 s_t_len,
436 );
437 }
438
439 tc_sum_ra += time_topp_ra;
440 tc_sum_lp += time_topp3_lp;
441 tf_sum_ra += t_motion_ra;
442 tf_sum_lp += t_motion_lp;
443 }
444
445 crate::verbosity_log!(
446 crate::diag::Verbosity::Summary,
447 "Average over {} experiments: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}",
448 n_exp,
449 tc_sum_ra / n_exp as f64,
450 tc_sum_lp / n_exp as f64,
451 tf_sum_ra / n_exp as f64,
452 tf_sum_lp / n_exp as f64,
453 );
454
455 Ok(())
456 }
457}